// NutriStat 
// Erik Knudsen
// 
// ----------------------------------------------------------------------------
// PURPOSE: This object is used to run the z-score and percentile calculations.
//  The main form will call the functions in this object and pass them various
//  data, such as age in months, gender, and also the basic measurement type,
//  and the functions return either the z-score or the percentile. Remember,
//  getting the z-score and percentile are the primary purposes of this tool,
//  so this is a very important part of the program.
// ----------------------------------------------------------------------------
//
// NOTE: This object has been deprecated and replaced with the AnthStat class
// library. Please do not update or use. Will remove in future build.
//
using System;
using System.Collections.Generic;
using System.Text;
using System.Data.OleDb;

namespace NutriStat
{
    // Object name: NutriDataCalcInternal
    public static class NutriDataCalcInternal
    {        
        // --------------------------------------------------------------------------
        // Function: GetZScore (decimal)
        // Purpose: Takes raw data inputs and produces a z-score based off of those
        //      inputs. The z-score is then returned to the calling function. This 
        //      function can report the z-score for any of the measurements we may
        //      need, and determines which one to use based on the measurementType
        //      parameter. Based on the value of measurementType, the function
        //      chooses the appropriate lookup table in CDC.mdb to grab the L,M,S
        //      values from.
        // --------------------------------------------------------------------------
        public static decimal GetZScore( decimal rawScore, decimal ageMonths, int gender, int measurementType ) 
        {
            // set defaults; assume BMI as the default
            string measurement = "BMI";
            string lookupcolumn = "Agemos";

            // here we begin to go through all of the possible measurementType inputs
            // and assign the measurement accordingly. The 'measurement' variable controls
            // which data table is read in the CDC.mdb file.

            // TODO: Probably should change this to a switch statement for easier readability. (Knudsen)
            if (measurementType == 0)
                measurement = "BMI";

            else if (measurementType == 1)
                measurement = "Head";

            else if (measurementType == 2)
                measurement = "HtAge";

            else if (measurementType == 3)
                measurement = "LgthAge";

            else if (measurementType == 4)
                measurement = "WtAge";

            // these last two need 'lookupcolumn'. Why? Because in all the preceding tables, Agemos is
            // the name of the column we need. In these two, however, its different and we need to account
            // for that.
            else if (measurementType == 5)
            {
                measurement = "WtHeight";
                lookupcolumn = "HeightCm";
            }

            else if (measurementType == 6)
            {
                measurement = "WtLength";
                lookupcolumn = "LengthCm";
            }

            // The L, M and S values are of statistical significace. Those wondering what they are should
            // use Google, Yahoo, Bing, etc. All we're concerned with is looking up the L, M and S values
            // from the right table in CDC.mdb and then using the function parameters to find the right
            // spot to pull them from. Here, we set the defaults for each of the values.
            decimal L = -1m;
            decimal M = -1m;
            decimal S = -1m;

            // Set a default value for the z-score.
            decimal Z = 1m;

            // This boolean controls whether or not we found a lookup value. Keep in mind that each row in
            // the database increments by 1 year, for example, 210.5 months goes to 211.5 months. This means
            // that if we have an age in months of 210.9 months, we will not have found a value and need to
            // interpolate. The foundValue bool controls whether we found a value or whether we need to 
            // interpolate.
            bool foundValue = false;

            // Open the DB connection.
            OleDbConnection m_dbConnection = new OleDbConnection(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=CDC.mdb;Persist Security Info=False");
            m_dbConnection.Open();

            // Create our query.
            OleDbCommand m_dbCommand = m_dbConnection.CreateCommand();
            m_dbCommand.CommandText = "SELECT L, M, S FROM " + measurement + " WHERE Sex = " + gender.ToString() + " AND " + lookupcolumn + " = " + ageMonths.ToString() + "";
            //m_dbCommand.CommandText = "SELECT L, M, S FROM BMI WHERE Sex = 1 AND AgeMos = 31.5";

            // execute the command
            OleDbDataReader m_dbReader = m_dbCommand.ExecuteReader();

            // Now we need to find out if our command found anything. Chances are it did not, since the lookup table
            // increments by 12 months (1 year) and there's a lot of possible values inbetween.
            while (m_dbReader.Read())
            {                
                // found something? Good! Assign L, M and S values from the DB to our internal 
                // decimal variables!
                L = Convert.ToDecimal(m_dbReader["L"]);
                M = Convert.ToDecimal(m_dbReader["M"]);
                S = Convert.ToDecimal(m_dbReader["S"]);

                foundValue = true;
            }

            // close our connection
            m_dbReader.Close();
            m_dbConnection.Close();

            // Do the math to get a z-score. This is based off the math we found on the Internet
            // that uses L, M and S values to generate a Z-score.
            Z = (rawScore / M);            
            Z = (decimal)Math.Pow(Convert.ToDouble(Z), Convert.ToDouble(L));
            Z = Z - 1;
            Z = Z / (L * S);

            // If we did NOT find a value in the lookup table, we need to interpolate. We call another function for this,
            // which handles the interpolation. (I suspect this could be done more efficiently.)
            if( !foundValue ) {
                Z = InterpZ(rawScore, ageMonths, gender, measurementType);
            }

            // return the Z score back to the calling function
            return Z;

        } // end GetZScore


        // --------------------------------------------------------------------------
        // Function: GetPercentile (decimal)
        // Purpose: Takes a z-score input and returns a percentile based off of that
        //      z-score.
        // --------------------------------------------------------------------------
        public static decimal GetPercentile( decimal Z )
        {
            // First, convert the Z (which is our z-score that we found using GetZScore) to
            // type double. 
            double z = Convert.ToDouble(Z);
            double y = 0;
            double alnorm = 0;

            // Below is an algorithm we use to calc the percentile off of the z-score. A lot of
            // places seem to recommend using lookup tables but I've instead used a formula; this
            // may be a bad idea, since the formula could be slowing down our program.
            // TODO: See if a lookup table is faster.
            // TODO: Get someone smart to review this to make sure it's as accurate as Epi Info 3 (www.cdc.gov/epiinfo)

            // Algorithm AS66 Applied Statistics (1973) vol22 no.3
            // Computes P(Z<z)

            //When z=1.96, the values returned are .975 (upper=false) or .025(upper=true) (roughly).
            //z = parseFloat(z);            
            bool upper = false;
            
            double ltone = 7.0;
            double utzero = 18.66;
            double con = 1.28;
            double a1 = 0.398942280444;
            double a2 = 0.399903438504;
            double a3 = 5.75885480458;
            double a4 = 29.8213557808;
            double a5 = 2.62433121679;
            double a6 = 48.6959930692;
            double a7 = 5.92885724438;
            double b1 = 0.398942280385;
            double b2 = 3.8052 * Math.Exp(-8.0);
            double b3 = 1.00000615302;
            double b4 = 3.98064794e-4;
            double b5 = 1.986153813664;
            double b6 = 0.151679116635;
            double b7 = 5.29330324926;
            double b8 = 4.8385912808;
            double b9 = 15.1508972451;
            double b10 = 0.742380924027;
            double b11 = 30.789933034;
            double b12 = 3.99019417011;

            if (z < 0)
            {
                upper = !upper;
                z = -z;
            }
            if (z <= ltone || upper && z <= utzero)
            {
                y = 0.5 * z * z;
                if (z > con)
                {
                    alnorm = b1 * Math.Exp(-y) / (z - b2 + b3 / (z + b4 + b5 / (z - b6 + b7 / (z + b8 - b9 / (z + b10 + b11 / (z + b12))))));
                }
                else
                {
                    alnorm = 0.5 - z * (a1 - a2 * y / (y + a3 - a4 / (y + a5 + a6 / (y + a7))));
                }
            }
            else
            {
                alnorm = 0;
            }
            if (!upper) alnorm = 1 - alnorm;

            // After all that math is done, take our decimal and return it to the calling function
            return Convert.ToDecimal(alnorm * 100);

        } // end GetPercentile

        // --------------------------------------------------------------------------
        // Function: InterpZ (decimal)
        // Purpose: Takes raw data inputs and produces a z-score based off of those
        //      inputs. This function is only called when the GetZScore function
        //      cannot find a lookup value. This function then uses interoplation,
        //      rather than a direct lookup, to find the right z-score. This 
        //      function can report the z-score for any of the measurements we may
        //      need, and determines which one to use based on the measurementType
        //      parameter. Based on the value of measurementType, the function
        //      chooses the appropriate lookup table in CDC.mdb to use for 
        //      interpolating values.
        // --------------------------------------------------------------------------
        private static decimal InterpZ(decimal rawBMI, decimal ageMonths, int gender, int measurementType) 
        {
            // TODO: This function is repeating a lot of work that we did in GetZScore, so at some
            // point I want to change this to be more efficient. (Knudsen)

            // set defaults; assume BMI as the default
            string measurement = "BMI";
            string lookupcolumn = "Agemos";

            // here we begin to go through all of the possible measurementType inputs
            // and assign the measurement accordingly. The 'measurement' variable controls
            // which data table is read in the CDC.mdb file.

            // TODO: Probably should change this to a switch statement for easier readability. (Knudsen)
            if (measurementType == 0)
                measurement = "BMI";

            else if (measurementType == 1)
                measurement = "Head";

            else if (measurementType == 2)
                measurement = "HtAge";

            else if (measurementType == 3)
                measurement = "LgthAge";

            else if (measurementType == 4)
                measurement = "WtAge";

            // these last two need 'lookupcolumn'. Why? Because in all the preceding tables, Agemos is
            // the name of the column we need. In these two, however, its different and we need to account
            // for that.
            else if (measurementType == 5)
            {
                measurement = "WtHeight";
                lookupcolumn = "HeightCm";
            }

            else if (measurementType == 6)
            {
                measurement = "WtLength";
                lookupcolumn = "LengthCm";
            }

            // The L, M and S values are of statistical significace. Those wondering what they are should
            // use Google, Yahoo, Bing, etc. All we're concerned with is looking up the L, M and S values
            // from the right table in CDC.mdb and then using the function parameters to find the right
            // spot to pull them from. Here, we set the defaults for each of the values.
            decimal L = -1m;
            decimal M = -1m;
            decimal S = -1m;

            // Unlike GetZScore, we need some upper and lower boundaries for L, M, and S. The _u is our 
            // upper, and _l is our lower.
            decimal L_u = -1m;
            decimal M_u = -1m;
            decimal S_u = -1m;

            decimal L_l = -1m;
            decimal M_l = -1m;
            decimal S_l = -1m;

            // Set a default value for z-score.
            decimal Z = 1m;

            // These are the upper/lower values for AgeMos, or if using WtHeight / WtLength, then for
            // HeightCm and LengthCm, respectively.
            decimal upper = 0m;
            decimal lower = 0m;

            // Now we start our interpolation routine. The first thing we need to do is set up our upper and
            // lower ageMonths boundaries. In reality, this is a simple math operation where we find out how
            // far our real AgeMonths value (the one passed in as a parameter) goes between two points, then
            // use the percentage (that is, if our lower boundary is 10 and our upper is 20, and the actual
            // value is 15, then the percentage is 50%) to assume L, M and S values. In code, this is a bit 
            // more complex. 
            decimal difference = 0m;

            // Next we truncate the ageMonths that was passed in as a parameter. Thus, if 234.2 is passed in,
            // wholeMonths gets assigned 234.
            decimal wholeMonths = Math.Truncate(ageMonths);

            // Difference stores the difference between the two. If 234.2 is passed in, we get 0.2. If 234.9 is
            // passed in, we get 0.9.
            difference = ageMonths - wholeMonths;

            // All the lookup values increment by 1, but all have .5 on the end. This complicates things further
            // and requires us to change our routine somewhat. What we do here is find the right lower and upper
            // boundaries, but we need to figure out where the closest upper and lower boundaries lie in the
            // lookup table first. So if we have 234.2 as our parameter, we need 234.5 and 233.5 as our upper/lower
            // values, respectively. If we have 234.9 as our parameter, we instead need 235.5 and 234.5. See?           
            if (difference < 0.5m ) {
                lower = wholeMonths - 0.5m;                
            }
            else {
                lower = wholeMonths + 0.5m;                
            }

            upper = lower + 1.0m; // The upper is so easy; we always add +1 to the lower.           

            // ****************
            // Get lower values
            OleDbConnection m_dbConnection = new OleDbConnection(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=CDC.mdb;Persist Security Info=False");
            m_dbConnection.Open();

            // We want to find the L, M, and S values for the lower limit here. We use the same logic as we did from GetZScore to do this.
            OleDbCommand m_dbCommand = m_dbConnection.CreateCommand();
            m_dbCommand.CommandText = "SELECT L, M, S FROM " + measurement + " WHERE Sex = " + gender.ToString() + " AND " + lookupcolumn + " = " + lower.ToString() + "";

            OleDbDataReader m_dbReader = m_dbCommand.ExecuteReader();
            while (m_dbReader.Read())
            {                
                L_l = Convert.ToDecimal(m_dbReader["L"]);
                M_l = Convert.ToDecimal(m_dbReader["M"]);
                S_l = Convert.ToDecimal(m_dbReader["S"]);             
            }
            m_dbReader.Close();
            m_dbConnection.Close();

            // ****************
            // Get upper values
            m_dbConnection = new OleDbConnection(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=CDC.mdb;Persist Security Info=False");
            m_dbConnection.Open();

            // Ah ha! Now, we find the L, M, and S values for the upper limit. We use the same logic as we did from GetZScore to do this.
            m_dbCommand = m_dbConnection.CreateCommand();
            m_dbCommand.CommandText = "SELECT L, M, S FROM " + measurement + " WHERE Sex = " + gender.ToString() + " AND " + lookupcolumn + " = " + upper.ToString() + "";

            m_dbReader = m_dbCommand.ExecuteReader();
            while (m_dbReader.Read())
            {                
                L_u = Convert.ToDecimal(m_dbReader["L"]);
                M_u = Convert.ToDecimal(m_dbReader["M"]);
                S_u = Convert.ToDecimal(m_dbReader["S"]);
            }
            m_dbReader.Close();
            m_dbConnection.Close();


            // The fun part comes next. Subtracting lower from ageMonths gives us the percent that ageMonths makes it when going
            // between lower and upper. If that doesn't make any sense, think of it this way: You have a 10-yard line and a 20-yard
            // line in American Football. Your team hikes the ball on the 10-yard line and advances 4 yards to the next down. So
            // then you're at the 14 yard line, correct? You just made it 40% of the way from the 10-yard line to the 20-yard line.
            // This uses the same concept.
            decimal percent = (ageMonths - lower);
            decimal percent_u = (upper - ageMonths);

            // But our overall percentage needs to equal 100 in order for this to work. We can't simply take the lower value. The
            // math needs us to do the same thing for both upper and lower L, M and S values and then add them together. So let's say
            // our percent (ageMonths - lower) was .4 meaning 40%. That means percent_u (upper - ageMonths) is .6, or 60%. Sixty
            // percent plus forty percent equals 100 percent, which is what we need to get an accurate value. So just like basic 
            // math, we add these two weighted L, M, S sets and add them and we get the 'final' L, M and S values we need.
            L = (L_u * percent) + (L_l * percent_u);
            M = (M_u * percent) + (M_l * percent_u);
            S = (S_u * percent) + (S_l * percent_u);

            // Now that we're done, we simply do the stat math to get the Z-score just like we did in GetZScore().
            Z = (rawBMI / M);            
            Z = (decimal)Math.Pow(Convert.ToDouble(Z), Convert.ToDouble(L));
            Z = Z - 1;
            Z = Z / (L * S);            

            // Return the z-score to the calling function.
            return Z;

        } // end InterpZ

    } // end class

} // end namespace
